/*
Subcomponent (main part) of SmileyTextArea,
  which is intended to be part of Eteria IRC Client from Javier Kohen
2001 written by Frank Bartels for Splendid Internet GmbH, Kiel, Germany

                        ===O=V=E=R=V=I=E=W===
                          (over this file)

                       Constants & Variables
                            Constructor
                           Private Utils
                           Update & Paint
                            Mouse Events
                     Component's User Interface
     Class Run  (a part of a line within attributes not changing)
  Class ContentLine  (a line of runs, is one or more lines on display)
*/

package com.splendid.awtchat;

import java.awt.*;
import java.awt.event.*;
import java.awt.image.ImageObserver;
import java.text.SimpleDateFormat;
import java.util.*;
import ar.com.jkohen.irc.MircMessage;
import ar.com.jkohen.awt.NickInfoPopup;

public class SmileyTextAreaArea extends Canvas implements MouseListener, MouseMotionListener, AdjustmentListener, FocusListener, ComponentListener
{

  //=======================================================================
  //                       Constants & Variables
  //=======================================================================

  private int bufferlen = 200; // number of lines saved by default
                              // (virtual lines, i.e. lines independent of display line length)

  private int dimx = 300; // reflects actual component width (initalized in constructor)
  private int dimy = 200; // reflects actual component height (initalized in constructor)
  private int borderx = 4;
  private int bordery = 3;
  private int dimxv = 294; // view area
  private int dimyv = 196;
  int subsequentindent = 12; // indent for subsequent lines (inside one append with long string)
  Vector tabs;

  int SMILEY_DEFAULT_SIZE = 14;
  int INTER = 6;

  int paint_i = 0;
  int mode = SmileyTextArea.FAST; // FAST is default
  int oldSbValue = 0;
  boolean SB_WORKAROUND_ENABLED = false;
  Scrollbar sb = null;
  int sbValue = 0;
  int sbLength = 0;
  int offset = 0;
    
  static int WAITCOUNT = 0; // this constant is calculated on construction
  boolean inupdate = false;
  boolean inappend = false;
  boolean last_action_scrolling = false;
  boolean canBreak = true;
  boolean isSelectable = false;

  HyperlinkReceiver hyperlinkReceiver = null;
  CopyText copyText = null;
  Vector lines = null;
  SmileyTextArea sta = null;
  Dimension preferreddim; // reflects preferred size
    
  // Color stuff
  static Color [] fixedColors = new Color[16]; // Init. in static block.
  private int urlColorIndex = 12;  // Standard url color

  private static final int bgColorIndex = 0; //16;  // background color index
  private static final int fgColorIndex = 1; //17;  // foreground color index

  Color backgroundColor;
  Color foregroundColor;
  Color mouseoverColor;
  Color selectedColor;
  
  // the default values are overwritten on construction
  private Font usedFonts[] = {null, null, null, null};
  private FontMetrics fmFonts[] = {null, null, null, null};
  private int fmSpaceFonts = 1;
  private int fmDescentFonts[] = {1, 1, 1, 1};
  private int fmAscentFonts[] = {1, 1, 1, 1};
  private int fmDY = 2;
  private int fmDescent = 1;
  private int fmAscent = 1;
  private int fmLeading = 0;
  
  // current settings of Graphics
  private int currentCombinedBoldItalic = 0;
  private boolean currentUnderlined = false;
  private int currentColorIndex = 0;

  private Vector contentLines; // instance variable for the list of ContentLines
  
  private Image img_buff;
  private Graphics gfx_buff;
  
  private NickInfoPopup nick_pop;
  private Point mouse_coords;
    

  //=======================================================================
  //                            Constructor
  //=======================================================================


  public void adjustmentValueChanged(AdjustmentEvent e)
  {
  	setRawSbValue(e.getValue());
  }
  
  public void focusGained(FocusEvent e)		
  {
//	System.out.println("focusGained");
											// if above container gets the focus...
  	last_action_scrolling = false;			// ...redisplay, because hidden areas...
	repaint();								// ...trash the view partially.
  }

  public void focusLost(FocusEvent e)
  {
  }
  
  public void componentResized(ComponentEvent e)
  {
  	dimensionInit();
  }
  
  public void componentHidden(ComponentEvent e)
  {
  }
  
  public void componentMoved(ComponentEvent e)
  {
  }
  
  public void componentShown(ComponentEvent e)
  {
	dimensionInit();
		
  	last_action_scrolling = false;
    repaint();
  }

  public SmileyTextAreaArea(SmileyTextArea sta, Scrollbar sb, HyperlinkReceiver hr, CopyText cp, NickInfoPopup np)
  {
  	super();
    this.sta = sta;
    this.sb = sb;
    hyperlinkReceiver = hr;
	copyText = cp;
    this.nick_pop = np;
    fmDY = 1;

    SB_WORKAROUND_ENABLED = ((new Scrollbar(Scrollbar.HORIZONTAL, 20, 10, 0, 20)).getValue() == 20);

    backgroundColor = fixedColors[bgColorIndex];
    super.setBackground(backgroundColor); // for consistency and against flickering
    foregroundColor = fixedColors[fgColorIndex];
    super.setForeground(foregroundColor); // for consistency only

//	prepareFont(getFont());  getFont()==null at this point of time
    prepareFont(sta.DEFAULT_FONT.getName(), sta.DEFAULT_FONT.getSize());

    contentLines = new Vector();

    sbLength = 0;
    sbValue = 0;
    correctSb();
    sb.addAdjustmentListener(this);

    addFocusListener(this);				// Does not work since this does not get the keyboard focus... see focus_frame
    addComponentListener(this);
    dimensionInit();					// Initialize dimension by same means
    addMouseListener(this);
    addMouseMotionListener(this);
  }

  private Container focus_element = null;	// Sort of passive construction step...
  
  public void addNotify()
  {
  	super.addNotify();
	if (focus_element == null)
    {
		focus_element = getContainer();
		if (focus_element != null)
			focus_element.addFocusListener(this);
	}
  }
  
  public Container getContainer()
  {
  	Container c = getParent();
    do
    {
		if (c != sta && c instanceof java.awt.Container)
			return ((java.awt.Container)c);
      	else
			c = c.getParent();
			
    } while (c != null);
    return null;
  }

  //=======================================================================
  //                            Private Utils
  //=======================================================================

  private void correctSb()
  {
  	int len = dimyv / fmDY;
    int scrollmax = sbLength - len;
	if (scrollmax < 0)
		scrollmax = 0;
    if (sbValue > scrollmax)
		sbValue = scrollmax; // needed for dimensionInit
    if (sbValue < 0)
		sbValue = 0;
    int value = scrollmax - sbValue;
	if (value < 0)
		value = 0;
//    sb.setValues(value * fmDY, len * fmDY, 0, (SB_WORKAROUND_ENABLED ? scrollmax : scrollmax + len) * fmDY);
	sb.setValues(value, len, 0, (SB_WORKAROUND_ENABLED ? scrollmax : scrollmax + len));
  }
  
  // scrolling called from outside
  
  public void setSbTop()
  {
  	setRawSbValue(0);
  }
  
  private void setRawSbValue(int value)
  {
	// this synchonization is not very critical and it costs too much time:
	// to be exact, it should be synchronized, because called from outside
	
//	offset = fmDY - (value % fmDY);
	offset = 0;
//	value = value / fmDY;
	
//	synchronized(this)
//	{
		int len = dimyv / fmDY;
		int scrollmax = sbLength - len;
		if (scrollmax < 0)
			scrollmax = 0;
		int sbValueOld = sbValue;
		sbValue = scrollmax - value;
		if (sbValue < 0)
			sbValue = 0;
//	}
    if (sbValueOld != sbValue)
    {
		// (at end of paint())
//		last_action_scrolling = true;
		last_action_scrolling = false;

		repaint();
		correctSb();
    }
  }

  private void prepareFont(String name, int size)
  {
  	usedFonts[0] = new Font(name, Font.PLAIN, size);
    usedFonts[1] = new Font(name, Font.BOLD, size);
    usedFonts[2] = new Font(name, Font.ITALIC, size);
    usedFonts[3] = new Font(name, Font.BOLD | Font.ITALIC, size);
    fmDY=0;
    fmDescent=0;
    fmAscent=0;
    fmLeading=9999;
    for (int i=0; i<4; i++)
    {
    	FontMetrics fm=getFontMetrics(usedFonts[i]);
    	fmFonts[i]=fm;
    	fmSpaceFonts+=fm.stringWidth(" ");
    	fmDescentFonts[i]=fm.getMaxDescent();
    	fmAscentFonts[i]=fm.getMaxAscent();
    	//if (fmDY<fm.getHeight()) fmDY=fm.getHeight();                   // fmDY is max.(height)
    	if (fmDescent<fm.getMaxDescent()) fmDescent=fm.getMaxDescent(); // fmDescent is max.
    	if (fmAscent<fm.getMaxAscent()) fmAscent=fm.getMaxAscent();   // fmAscent is max.
    	if (fmLeading>fm.getLeading()) fmLeading=fm.getLeading();     // fmLeading is min.
    }
    if (fmLeading<0) fmLeading=0;
    fmDY=fmDescent+fmAscent+fmLeading + INTER; // construction of abstract fm.getHeight() for all fonts
    fmSpaceFonts=(fmSpaceFonts+2)/4; // +2 in order to round; now fmSpaceFonts is mean value
    if (fmDY<fmDescent+fmAscent+fmLeading) // fmDY could be too small!
      fmDY=fmDescent+fmAscent+fmLeading;
    correctSb();
  }

  private synchronized void dimensionInit()
  {
//  	(this) // must be synchronized, because called from outside
//    {
		Dimension d = getSize();
		dimx = d.width;
		dimy = d.height;
		dimxv = dimx - borderx * 2;
		dimyv = dimy - bordery * 2;
		
		sbLength = 0;
		for (int i = contentLines.size() - 1; i >= 0; i--)
		{
			ContentLine cl = (ContentLine)contentLines.elementAt(i);
			cl.reconstruct();
			sbLength += cl.lineDY;
      }
      if (sbValue > sbLength)
	  	sbValue = sbLength;
      correctSb();
//    }
    last_action_scrolling = false;
    invalidate();
    repaint();
  }

/*
	private Object[] getContentLine()
	{
		if (usedFonts[3] == null)
			return(null);
  
		p = new Point(p.x, p.y - offset);
		int y0 = dimy - bordery + sbValue * fmDY;
   
		if ((p.y > dimy - bordery) || (p.y < bordery))
			return(null);
   
		for (int i = contentLines.size() - 1; i >= 0; i--)
		{
			ContentLine cl = (ContentLine)contentLines.elementAt(i);
			y0 -= cl.lineDY * fmDY;
			if (p.y > y0)
			{
				Object o[] = new Object[] { cl, new Integer(y0) };
				return(o);
			}
		}
		return(null);
	}
*/
	private ContentLine getContentLine(Point p)
	{
		if (usedFonts[3] == null)
			return(null);
  
		p = new Point(p.x, p.y - offset);
		int y0 = dimy - bordery + sbValue * fmDY;
   
		if ((p.y > dimy - bordery) || (p.y < bordery))
			return(null);
   
		for (int i = contentLines.size() - 1; i >= 0; i--)
		{
			ContentLine cl = (ContentLine)contentLines.elementAt(i);
			y0 -= cl.lineDY * fmDY;
			if (p.y > y0)
				return(cl);
		}
		return(null);
	}
	
	private int getContentLineY(Point p)
	{
		if (usedFonts[3] == null)
			return(0);
  
		p = new Point(p.x, p.y - offset);
		int y0 = dimy - bordery + sbValue * fmDY;
   
		if ((p.y > dimy - bordery) || (p.y < bordery))
			return(0);
   
		for (int i = contentLines.size() - 1; i >= 0; i--)
		{
			ContentLine cl = (ContentLine)contentLines.elementAt(i);
			y0 -= cl.lineDY * fmDY;
			if (p.y > y0)
				return(y0);
		}
		return(0);
	}
	
	private Run getRun(Point p) // searching the run is analogous paint
	{
		p = new Point(p.x, p.y - offset);
 
		ContentLine cl = getContentLine(p);
		if (cl != null)
		{
			int y0 = getContentLineY(p) + fmLeading;
		
			// cl found, now search the run
			int y = p.y - y0;
			int dyline = fmAscent + fmDescent;
    
			for (Enumeration e = cl.runs.elements(); e.hasMoreElements(); )
			{
				Run run = (Run)e.nextElement();
				int y2 = run.pixY0 + dyline + INTER;
 
				if (run.pixY0 == run.pixY1 && y < y2 && y > run.pixY1)  // only one line, check X0 and X1
				{
					if (p.x > run.pixX0 && p.x < run.pixX1) return(run);
				}
				else if (run.pixY0 != run.pixY1 && y < y2 && y < run.pixY1)  // line broken, first line, check X0
				{
					if (p.x > run.pixX0) return(run);
				}
				else if (run.pixY0 != run.pixY1 && y > y2 && y < run.pixY1)  // line broken, between first and last line
				{
					if (p.x > run.pixX0) return(run);
				}
				else if (run.pixY0 != run.pixY1 && y > y2 && y > run.pixY1 && y < run.pixY1 + y2)  // line broken, last line, check X1
				{
					if (p.x > run.pixX0 && p.x < run.pixX1) return(run);
				}
			}
		}
  
		return(null);
	}

  private final void deleteFirstLine()
  {
  	if (contentLines.isEmpty())
		return;
    sbLength -= ((ContentLine)(contentLines.firstElement())).lineDY;
    contentLines.removeElementAt(0);
    if (paint_i > 0)
		paint_i--;				// undo the effect of removeElementAt for paint if paint runs
    if (sbValue > sbLength)
		sbValue = sbLength; 	// not really nescessary...
    correctSb();
  }
  //=======================================================================
  //                           Update & Paint
  //=======================================================================

    private void createImageBuffer(int width, int height)
	{
		if (width < 1)
			width = 1;
		if (height < 1)
			height = 1;
			
		this.img_buff = createImage(width, height);
		if (gfx_buff != null)
			gfx_buff.dispose();

		if (img_buff != null)
			this.gfx_buff = img_buff.getGraphics();
		else
			this.gfx_buff = getGraphics();
    }
	
	
  public void update(Graphics g)
  {
  	inupdate = true;
    paint(g);	// everything in paint, no clear, but no need to call from here (no effect)
    inupdate = false;
  }

public synchronized void paint(Graphics g)
{
	if (gfx_buff == null || img_buff.getWidth(this) != dimx || img_buff.getHeight(this) != dimy)
	{
		createImageBuffer(dimx, dimy);
		paintBorder(g);
	}
			
  	if (usedFonts[3] == null)
	{
		Font f = getFont();
		if (f == null)
			return; // must have a font...
		prepareFont(f.getName(), f.getSize());
	}


    int dyLines = sbValue - oldSbValue;
    int dy = dyLines * fmDY;
    oldSbValue = sbValue;
    boolean scrolling = (Math.abs(dy) < (dimy * 2) / 3);
    
    if (!inupdate && !inappend)
    	scrolling = false; 	// scrolling only in update() or append()
    	
    if (!last_action_scrolling)
    	scrolling = false; 	// scrolling only if scrolling or append
		
    if (mode == SmileyTextArea.FAST)
        scrolling = false;      // scrolling is possibly not safe
	
    if (!isShowing())
    	scrolling = false; 	// may be overlapped by another window


	if (scrolling && dy != 0)	
    {
    	if (mode == SmileyTextArea.SMOOTH)
    	{
    		int step = (dy / fmDY);	// so far exact divison (rest == 0)
    		for (int yy = 0; Math.abs(yy) < Math.abs(dy); yy += step)
	        {
				if (yy != 0)
				{
					try
					{
						Thread.sleep(5);
					} catch (InterruptedException e) {}
				}

				if (dy < 0)
				    gfx_buff.copyArea(2, -step + 2 + (yy - dy + step), dimx - 4, dimy - 4 + step + (dy - step), 0, step); // up
				else
				    gfx_buff.copyArea(2, 2 + yy, dimx - 4, dimy - 4 - step - (dy - step), 0, step); // down
				
				//area a littlebit too large (OK, but funny optics):
//				if (dy < 0)
//					g.copyArea(2, -step + 2, dimx - 4, dimy - 4 + step, 0, step); // up
//				else
//					g.copyArea(2, 2, dimx - 4, dimy - 4 - step, 0, step); // down
					
				
				g.drawImage(img_buff, 0, 0, backgroundColor, this);
			}
		}
		else
		{
			if (dy < 0)
				gfx_buff.copyArea(2, -dy + 2, dimx - 4, dimy - 4 + dy, 0, dy); // up
			else
				gfx_buff.copyArea(2, 2, dimx - 4, dimy - 4 - dy, 0, dy); // down
				
			g.drawImage(img_buff, 0, 0, backgroundColor, this);
		}
    }

    if (bordery > 2)
    {
    	gfx_buff.setColor(backgroundColor);
		gfx_buff.fillRect(2, dimy - bordery, dimx - 4, bordery - 2);
    }
	
    int lineRevY = -sbValue;
    int maxLineRevY = (dimy - 4) / fmDY; // top
    int minLineRevY = 0; // bottom

    if (scrolling && dyLines < 0)
    	maxLineRevY = -dyLines - 1;
		
    if (scrolling && dyLines >= 0)
    	minLineRevY = (dimy - 2 * bordery - 2 - dy) / fmDY;
		

	for (int i = 0; i < contentLines.size(); i++)
	{
		ContentLine cl = (ContentLine)contentLines.elementAt(i);
		cl.setMouseOver(false);
	}
	if (mouse_coords != null)
	{
		ContentLine cl = getContentLine(mouse_coords);
		if (cl != null)
			cl.setMouseOver(true);
	}
				
	for (paint_i = contentLines.size() - 1; paint_i >= 0; paint_i--)
	{
	  	ContentLine cl = (ContentLine)contentLines.elementAt(paint_i);

		if (lineRevY + cl.lineDY - 1 < minLineRevY)
			lineRevY += cl.lineDY; // below (skip)
		else
			lineRevY = cl.paint(gfx_buff, lineRevY);

		if (lineRevY > maxLineRevY)
			break; // above (we are ready)
	}
	
	paint_i = 0;
    if (!(scrolling && dyLines <= 0)) // avoid clearing the top if scrolling up
    {
    	int topdrawn = dimy - bordery - (lineRevY) * fmDY;
		if (topdrawn > 2) // clear rest above textlines, if  there is a rest
		{
			gfx_buff.setColor(backgroundColor);
			gfx_buff.fillRect(2, 2, dimx - 4, topdrawn);
      }
    }
    

	if (isShowing())
	{
	    paintRollover(gfx_buff);
		g.drawImage(img_buff, 0, 0, backgroundColor, this);
	}

	last_action_scrolling = true;
}

	private void paintBorder(Graphics g)
	{
    	gfx_buff.setColor(Color.gray);
        int tempx1[] = {0, 0, dimx - 1};
        int tempy1[] = {dimy - 1, 0, 0};
        gfx_buff.drawPolyline(tempx1, tempy1, 3);
        
        gfx_buff.setColor(Color.black);
        int tempx2[] = {1, 1, dimx - 2};
        int tempy2[] = {dimy - 2, 1, 1};
        gfx_buff.drawPolyline(tempx2, tempy2, 3);
        
        gfx_buff.setColor(Color.white);
        int tempx3[] = {0, dimx - 1, dimx - 1};
        int tempy3[] = {dimy - 1, dimy - 1, 0};
        gfx_buff.drawPolyline(tempx3, tempy3, 3);
    	
    	gfx_buff.setColor(Color.lightGray);
        int tempx4[] = {1, dimx - 2, dimx - 2};
        int tempy4[] = {dimy - 2, dimy - 2, 1};
        gfx_buff.drawPolyline(tempx4, tempy4, 3);
		
		gfx_buff.setClip(2, 2, dimx - 4, dimy - 4);
	}
	
	private void paintRollover(Graphics g)
	{
		nick_pop.setVisible(false);
		
		if (mouse_coords != null)
		{
			String n = null;
			ContentLine cl = getContentLine(mouse_coords);
			if (cl != null)
			{
				cl.setMouseOver(true);
				
				n = cl.nick;
				if (n != null)
				{
					nick_pop.setNick(n);
					nick_pop.setVisible(true);
				}
			}
			
			nick_pop.setLocation(new Point(mouse_coords.x + 32, mouse_coords.y + nick_pop.getSize().height));
		}

		nick_pop.paint(g);
	}
  
  //=======================================================================
  //                            Mouse Events
  //=======================================================================

  public void mousePressed(MouseEvent ev)
  {
  }
  
  public void mouseReleased(MouseEvent ev)
  {
  		// This is the left click to select a line of text
		
		if ((ev.getModifiers() & MouseEvent.BUTTON3_MASK) == 0 && isSelectable)
		{
			ContentLine clicked_cl = getContentLine(ev.getPoint());
			if (clicked_cl != null)
				clicked_cl.setMouseOver(true);
			
			for (int i = 0; i < contentLines.size(); i++)
    		{
		    	ContentLine cl = (ContentLine)contentLines.elementAt(i);
				if (clicked_cl == cl)
					cl.setSelected(!cl.isSelected());
				else
					cl.setSelected(false);
		    }
			
			last_action_scrolling = false;
			repaint();
		}

		// This is the right click to get a cut-paste window
				
		if (ev.isPopupTrigger() || (ev.getModifiers() & MouseEvent.BUTTON3_MASK) != 0)
		{
			String s = "";
			for (int i = 0; i < contentLines.size(); i++)
    		{
		    	ContentLine cl = (ContentLine)contentLines.elementAt(i);
				s = s + cl.text + "\n";
		    }
			if (copyText != null)
				copyText.addText(s);
		}
  }
  
  public void mouseEntered(MouseEvent ev)
  {
  	mouse_coords = ev.getPoint();
	last_action_scrolling = false;
	repaint();
  }
  
	public void mouseExited(MouseEvent ev)
	{
		mouse_coords = null;
		last_action_scrolling = false;
		repaint();
	}

  public void mouseClicked(MouseEvent ev)
  {
  	Run run = getRun(ev.getPoint());
	if (run != null)
	{
		if (!run.getUrl().equals("") && hyperlinkReceiver != null)
			hyperlinkReceiver.handleHyperlink(run.url);
			
		if (!run.getNick().equals("") && hyperlinkReceiver != null)
			hyperlinkReceiver.handleNick(run.nick);
	}
  }

	public void mouseMoved(MouseEvent ev)
	{
		mouse_coords = ev.getPoint();
		last_action_scrolling = false;
		repaint();

	  	Run run = getRun(mouse_coords);
	    if (run != null)
	    {
			if (!(run.getUrl().equals("") && run.getNick().equals("")) && hyperlinkReceiver != null)
			{
				setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
	        	return;
			}
	    }
	  	setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
	}

  public void mouseDragged(MouseEvent ev)
  {
  }

  //=======================================================================
  //                        Component's User Interface
  //=======================================================================

	public void append(String s, boolean interpretUrl)
	{
		addContent(s, interpretUrl, null);
	}

	public void append(String s, boolean interpretUrl, String n)
	{
		addContent(s, interpretUrl, n);
	}
	
	public void clean()
	{
	  	contentLines.removeAllElements();
		if (gfx_buff != null)
		{
			gfx_buff.dispose();
			gfx_buff = null;
		}
		dimensionInit();
	}
	
  public void addContent(String s, boolean interpretUrl, String n)
  {
	if (s == null || s.length() == 0)
		s = "\n";
	synchronized(this)
	{
  		ContentLine cl = new ContentLine(s, interpretUrl, n);
  		cl.reconstruct();
		contentLines.addElement(cl);
		
		sbLength += cl.lineDY;
//		sbValue = 0;
		if (oldSbValue == 0)
			oldSbValue += cl.lineDY;
		else
			oldSbValue = -999;
		correctSb();
		
		if (bufferlen >= 0 && contentLines.size() > bufferlen)
			deleteFirstLine();
	}
	
	if (sbValue == 0 && mode != SmileyTextArea.SAFE)
	{
		// (at end of paint())
//		last_action_scrolling = true;
		repaint();
	}

  }
  
  public void setMode(int mode)
  {
  	synchronized(this)
    {
    	this.mode = mode;
		last_action_scrolling = false;
    }
    repaint();
  }
  
  public void setBreaks(boolean canBreak)
  {
  	this.canBreak = canBreak;
  }

  public void addTab(int t)
  {
  	if (tabs == null)
  		tabs = new Vector();
  	tabs.addElement(new Integer(t));
  }
  
  public void setSubsequentIndent(int indent)
  {
  	synchronized(this)
    {
    	subsequentindent = indent;
		last_action_scrolling = false;
    }
    dimensionInit();
    // repaint(); (included in dimensionInit())
  }
  
  public int getSubsequentIndent()
  {
  	return(subsequentindent);
  }

  public void setBufferlen(int bufferlen)
  {
  	synchronized(this)
    {
		this.bufferlen = bufferlen;
		while (bufferlen >= 0 && contentLines.size() > bufferlen)
			deleteFirstLine();
    }
  }
  
  public int getBufferlen()
  {
  	return(bufferlen);
  }

  public void setBackground(Color c)
  {
  	synchronized(this)
    {
    	fixedColors[bgColorIndex] = c;
		backgroundColor = c;
		super.setBackground(backgroundColor); // for consistency and against flickering
		last_action_scrolling = false;
    }
    repaint();
  }
  
  public Color getBackground()
  { return(fixedColors[bgColorIndex]);
  }
  
  public void setForeground(Color c)
  { synchronized(this)
    { fixedColors[fgColorIndex]=c;
      foregroundColor=c;
      super.setForeground(foregroundColor); // for consistency only
      last_action_scrolling=false;
    }
    repaint();
  }
  
  public Color getForeground()
  {
	return(fixedColors[fgColorIndex]);
  }
  
  public void setSelectedBackground(Color c)
  {
    mouseoverColor = c;
  }
  
  public Color getSelectedBackground()
  {
	return(mouseoverColor);
  }

	public static void setColorPalette(Color palette[])
	{
		fixedColors = palette;
	}
  
  public void setColorPaletteEntry(int n, Color c)
  {
  	synchronized(this)
    {
    	fixedColors[(n % 16)] = c;
    	last_action_scrolling = false;
    }
    repaint();
  }
  
  public Color getColorPaletteEntry(int n)
  {
  	return(fixedColors[(n % 16)]);
  }

  public void changeFont(String name, int size)
  {
  	synchronized(this)
    {
    	prepareFont(name, size);
    	last_action_scrolling = false;
    }
    dimensionInit();
    // repaint(); (included in dimensionInit())
  }
  
  public Font getFont()
  {
	return(usedFonts[0]);
  }

  //=======================================================================
  //     Class Run  (a part of a line within attributes not changing)
  //=======================================================================

	// === class Run represents a text fragment including attributes ===
	
  private class Run implements ImageObserver
  { public String text = null;
    public String url = null;
    public String nick = null;
    public boolean display = true;
    public int pixX0 = 0; // Start(0)
    public int pixY0 = 0;
    public int pixX1 = 0; // End(1)
    public int pixY1 = 0;
    public int nbreaks = 0;
    public Vector breaks = null; // if the Run splits at lineends the text indices are saved here
    public int combinedBoldItalic = 0;
    public boolean underlined = false;
    public int fgColorIndexMine = fgColorIndex;
    public int bgColorIndexMine = bgColorIndex;
    public Image smiley = null;

	// Constructor (text)
	
    public Run(String atext, boolean bold, boolean italic, boolean underlined, int fgColorIndex, int bgColorIndex)
    {
		char c;
		int len = atext.length();
		text = "";
		for (int i = 0; i < len; i++)
		{
			c = atext.charAt(i);
			if (Character.isISOControl(c))
				continue; // skip control characters
			if (!Character.isDefined(c))
				continue; // skip undefined characters
			text += c;
		}
      	this.combinedBoldItalic = bold ? (italic ? 3 : 1) : (italic ? 2 : 0);
      	this.underlined = underlined;
      	this.fgColorIndexMine = fgColorIndex;
      	this.bgColorIndexMine = bgColorIndex;
    }
    
	// Constructor (smiley)
	
    public Run(String text, Image smiley, int bgColorIndex)
    {
    	this.text = text;
    	this.smiley = smiley;
    	this.bgColorIndexMine = bgColorIndex;
    }
    
	// if URL, you need this second construction step
	
    public void setUrl(String url)
    {
    	this.url = url;
    	// preserve bold and italic and bgColorIndexMine
    	underlined = true;
    	fgColorIndexMine = urlColorIndex;
    }

    public void setNick(String nick)
    {
    	this.nick = nick;
    }
    
    public void setDisplay(boolean b)
    {
    	this.display = b;
    }
    
    public boolean display()
    {
    	return(display);
    }
    
    private int mySpaceIndex(String s, int i)
    {
    	int len = s.length();
      	for (int j = i; j < len; j++)
        	if (Character.isSpaceChar(s.charAt(j)))
        		return(j);
        	
      	return(-1);
    }

    public int setXY(int newPixX, int newPixY, int line) // prepare the Run for display
    {
    	String s = text;
		int loopcount, lasti, i, x, snipped = 0;
		
      	if (newPixX >= dimxv - fmSpaceFonts * 2) // break Run, needed for speed and smileys
      	{
      		newPixX = borderx + subsequentindent;
        	newPixY += fmDY;
        	line++;
      	}
      	
      	breaks = null;
      	nbreaks = 0;
	    pixX0 = newPixX;
      	pixY0 = newPixY;
      	if (smiley != null)
      	{
      		pixX1 = newPixX + smiley.getWidth(null);
        	pixY1 = newPixY;
        	if (pixX1 > dimx - borderx)
        	{
        		pixX0 = borderx + subsequentindent;
          		pixY0 += fmDY;
          		pixX1 = borderx + subsequentindent + smiley.getWidth(null);
          		pixY1 += fmDY;
          		line++;
        	}
        	return(line); // smiley case ready
      	}
      
		pixX1 = newPixX + fmFonts[combinedBoldItalic].stringWidth(s);
		pixY1 = newPixY;
      
		if (smiley != null)
			return(line); // do not break inside a smiley
      	
		loopcount = 0;
		while (pixX1 > dimx - borderx) // then must break line
		{
			if (loopcount++ > 100)
				break; // avoid deadlocks (although there are none ;-)
			
			for (lasti = 0, i = 0; (i = mySpaceIndex(s, i)) >= 0; )
        	{
				x = newPixX + fmFonts[combinedBoldItalic].stringWidth(s.substring(0, i));
				if ( x > dimx - borderx)
					break; // found proper break (the last one on actual line)
				i++; // pos of next char after space
//				while (i < s.length() && Character.isSpaceChar(s.charAt(i)))
//					i++; // hop over spaces
         		 lasti = i;
        	}
			if (lasti == 0) // break complete s
			{
				if (newPixX <= borderx + subsequentindent) // already gone to the next line?
				{
					// the (spaceless) s does not fit on a line! break by force somewhere
					for (lasti = 1, i = 1; i < s.length() - 1; i++)
					{
						x = newPixX + fmFonts[combinedBoldItalic].stringWidth(s.substring(0, i + 1));
						if (x > dimx - borderx)
						{
							lasti = i;
							break;
						}
					}
				}
				else
				{
					newPixX = borderx + subsequentindent;
					if (pixY0 == pixY1 && breaks == null) // if break at beginning of run...
					{
						pixX0 = borderx + subsequentindent; // ...the beginning also must go on next line
						pixY0 += fmDY;
					}
					pixX1 = newPixX + fmFonts[combinedBoldItalic].stringWidth(s);
					pixY1 += fmDY;
					line++;
					continue; // retry with linebreak (almost nothing to do for this) before
				}
			}
        
			if (lasti > 0 && lasti < s.length()) // break line (can not use else!)
			{
				if (!canBreak)
					return(line);
    	    	
				if (breaks == null)
					breaks = new Vector();
				breaks.addElement(new Integer(lasti + snipped));
				nbreaks++;
				s = s.substring(lasti);
				snipped += lasti;
			}
        
			newPixX = borderx + subsequentindent;
			pixX1 = newPixX + fmFonts[combinedBoldItalic].stringWidth(s);
			pixY1 += fmDY;
			line++;
		}
      
		return(line);
	}

    private void print(Graphics g, String s, int x, int y)
    {
    	if (smiley != null)
    	{
    	  	int dy=smiley.getHeight(this); if (dy==0) dy=SMILEY_DEFAULT_SIZE;
    	    int ascent=fmAscentFonts[combinedBoldItalic];
    	    int descent=fmDescentFonts[combinedBoldItalic];
    	    int yoff=-(ascent-descent+dy)/2; // put in center between ascent and descent
    	    if (yoff+dy-1>descent) yoff=descent-dy+1;  // but not below descent

    	    if (bgColorIndexMine!=bgColorIndex) // Not the normal background color->paint bg first
    	    {
    	    	int len=smiley.getWidth(this); if (len==0) len=SMILEY_DEFAULT_SIZE;
    	    	g.setColor(fixedColors[bgColorIndexMine]);
       			g.fillRect(x, y-ascent, len, ascent + descent);
                                  // +1 because rectangle goes only to n-1 on bottom
                                  // -1 because from -ascent to descent (incl) is 1 too much
          		g.setColor(fixedColors[fgColorIndexMine]);
        	}

        	g.drawImage(smiley, x, y + yoff, this);
      	}
      	else
      	{
      	  	if (bgColorIndexMine!=bgColorIndex) // Not the normal background color->paint bg first
      	  	{
      	  		int len=fmFonts[combinedBoldItalic].stringWidth(s);
      	    	int ascent=fmAscentFonts[combinedBoldItalic];
      	    	g.setColor(fixedColors[bgColorIndexMine]);
      	    	g.fillRect(x, y-ascent, len, ascent + fmDescentFonts[combinedBoldItalic]);
                                  // +1 because rectangle goes only to n-1 on bottom
                                  // -1 because from -ascent to descent (incl) is 1 too much
                g.setColor(fixedColors[fgColorIndexMine]);
        	}

	        g.drawString(s, x, y);

        if (underlined) // underlined->add the line
        {
        	int len=fmFonts[combinedBoldItalic].stringWidth(s);
        	int below=fmDescentFonts[combinedBoldItalic]/4;
          	if (below<1) below=1; // otherwise the underline line sticks to the font
          	// if (getItalic())
          	//   would like to subtract something from len, but I don't know how much
          	g.drawLine(x,y+below,x+len-1,y+below); // -1 makes it comaptible with fillRect len
          	if (getBold())
            	g.drawLine(x,y+below+1,x+len-1,y+below+1);
        }
      }
    }
    
	public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h)
	{
		last_action_scrolling = false;
		repaint();
		return(true);
	}
	
    public void paint(Graphics g, int y0, boolean force)
    {
		int x = pixX0;
		int y = y0 + pixY0;
		setMyGraphicsAttributes(g, force);
		if (nbreaks == 0) // makes simple case fast
		{
			print(g, text, x, y);
		}
		else
		{
			int oldj = 0;
			int j = 0;
			for (int i = 0; i <= nbreaks; i++)
			{
				if (i == nbreaks)
					j = text.length();
				else
					j = ((Integer)(breaks.elementAt(i))).intValue();
				print(g, text.substring(oldj, j), x, y);
				x = borderx + subsequentindent;
				y += fmDY;
				oldj = j;
        	}
      	}
	}

    public void setMyGraphicsAttributes(Graphics g, boolean force)
    {
    	if (currentCombinedBoldItalic != combinedBoldItalic || force)
    	{
    		g.setFont(usedFonts[combinedBoldItalic]);
        	currentCombinedBoldItalic = combinedBoldItalic;
      	}
      	if (currentColorIndex != fgColorIndexMine || force)
      	{
      		g.setColor(fixedColors[fgColorIndexMine]);
        	currentColorIndex = fgColorIndexMine;
      	}
      	// currentUnderlined != underlined does not matter for g
    }

    public String getUrl()
    {
		return(url == null ? "" : url);
    }
    
    public String getNick()
    {
		return(nick == null ? "" : nick);
    }
	
    public boolean getBold()
    {
		return((combinedBoldItalic & 1) == 1);
    }
    public boolean getItalic()
    {
		return((combinedBoldItalic & 2) == 2);
    }
	
    public boolean getUnderlined()
    {
		return(underlined);
    }
    public Color getColor()
    {
		return(fixedColors[fgColorIndexMine]);
    }
    public Font getFont()
    {
		return(usedFonts[combinedBoldItalic]);
    }
    public Image getSmiley() // may return null!
    {
		return(smiley);
    }

  } // ============ End of class Run ============

  //=======================================================================
  //  Class ContentLine  (a line of runs, is one or more lines on display)
  //=======================================================================

  private class ContentLine // === class ContentLine represents an abstract line of text ===
  { public String text;
    public Vector runs;
    public int lineStart;
    public int lineEnd;
    public int lineDY=1;
   	public String nick;
	public boolean mouse_over;
	public boolean line_selected;
	public Date time_stamp;
	
	// Constructor (parses string and produces runs)

    public ContentLine(String s, boolean interpretUrl, String n)
    {
    	boolean boldCurrent = false;
		boolean italicCurrent = false;
		boolean underlineCurrent = false;
		int bgColorIndexCurrent = bgColorIndex;
		int fgColorIndexCurrent = fgColorIndex;

		if (s.length() == 0)
			return; // empty line, no runs
			
		time_stamp = new Date();
		if (false)
		{
			SimpleDateFormat date_format = new SimpleDateFormat("[HH:mm:ss]");
			s = date_format.format(time_stamp) + " " + s;
		}
			
		// runs.addElement(new Run(s,false,false,false,fgColorIndex,bgColorIndex));
		//                        (simple version (without parsing))
		text = s;
		nick = n;
		runs = new Vector();

      
      int hrefend = -1;
      int start = 0;
      int len = s.length();
      for (int i = 0; i < len; i++)
      {
	  	char ch = s.charAt(i);
	  	
        // find potential href's end and check for email
/*
        if (i > hrefend)
        {
			hrefend = findEndOfHref(s, i);
			if (interpretUrl && hrefend < len && i < hrefend)
			{
				// is email address ?
			
               if(s.charAt(hrefend) == '@')
               {
               		if (start < i)
                		runs.addElement(new Run(s.substring(start, i), boldCurrent, italicCurrent, underlineCurrent, fgColorIndexCurrent, bgColorIndexCurrent));
                		
   					int emailend = findEndOfHref(s, hrefend + 1);
   					String emailstr = s.substring(i, emailend);
   					Run run = new Run(emailstr, boldCurrent, italicCurrent, underlineCurrent, fgColorIndexCurrent, bgColorIndexCurrent);
   				
                 	// add protocol mailto: to URL saved in run if missing so far
                 	
                 	if (emailstr.length() >= 7)
                 	{
						emailstr = "mailto:" + emailstr;
                 	}
                 	else
                 	{
                   		if (!emailstr.substring(0, 7).toUpperCase().equals("MAILTO:"))
                     		emailstr = "mailto:" + emailstr;
                    }
                 	run.setUrl(emailstr);
                 	runs.addElement(run);
                 	i = emailend - 1;
                 	start = i + 1;
                 	continue;
               }
            }
        }
*/
        // check for href (including #channel)
        if (interpretUrl && isHref(s, i))
        {
			if (start < i)
				runs.addElement(new Run(s.substring(start,i), boldCurrent, italicCurrent, underlineCurrent, fgColorIndexCurrent, bgColorIndexCurrent));
			
			hrefend = findEndOfHref(s, i);
          	String urlstr = s.substring(i, hrefend);
	        Run run = new Run(urlstr, boldCurrent, italicCurrent, underlineCurrent, fgColorIndexCurrent, bgColorIndexCurrent);
          
      	    // add protocol http:// to URL saved in run if missing so far
          
        	if (urlstr.length() >= 4)
            	if (urlstr.substring(0, 4).toUpperCase().equals("WWW."))
            	  urlstr = "http://" + urlstr;
            	  
//			if (urlstr.length() >= 8 && urlstr.indexOf("://") == -1)
//				if (urlstr.indexOf("@") > 0)
//					urlstr = "mailto:" + urlstr;

          	run.setUrl(urlstr);
          	runs.addElement(run);
          	i = hrefend - 1;
          	start = i + 1;
          	continue;	
        }

        // check for smileys
        
		if (sta.smileys != null)
		{
			int imax = i + sta.maxCodeLength;
			if (imax > len)
				imax = len;
			String smax = s.substring(i, imax);
			Image image;
			while (smax.length() > 0)
			{
				if ((image = (Image)sta.smileys.get(smax)) != null)		// found (longest) smiley
				{
					if (start < i)
						runs.addElement(new Run(s.substring(start, i), boldCurrent, italicCurrent, underlineCurrent, fgColorIndexCurrent, bgColorIndexCurrent));
					runs.addElement(new Run(smax, image, bgColorIndexCurrent));
					i += smax.length() - 1;
					start = i + 1;
					break;
				}
				smax = smax.substring(0, smax.length() - 1);
			}
		}
        
        // check for control codes
        
        switch(ch)
        { case MircMessage.BELL:
            Toolkit.getDefaultToolkit().beep();
            break;
          case MircMessage.BOLD:
          case MircMessage.ITALIC:
          case MircMessage.UNDERLINE:
          case MircMessage.COLOR:
          case MircMessage.REVERSE:
          case MircMessage.RESET:        // is control code
            if (start<i)
              runs.addElement(new Run(s.substring(start,i), boldCurrent,italicCurrent,underlineCurrent, fgColorIndexCurrent,bgColorIndexCurrent));
            start=i+1;
            switch(ch)
            { case MircMessage.BOLD:
                boldCurrent=!boldCurrent;
                break;
              case MircMessage.ITALIC:
                italicCurrent=!italicCurrent;
                break;
              case MircMessage.UNDERLINE:
                underlineCurrent=!underlineCurrent;
                break;
              case MircMessage.COLOR:
                // parse color(s) 0,0 ... 15,15 (... 99,99 mapped modulo 16)
                { int j,k;
                  char c;
                  int fgv=-1;
                  int bgv=-1;
                  for (j=i+1; j<len && j<=i+2; j++)
                  { c=s.charAt(j);
                    if (!Character.isDigit(c)) break;
                    if (fgv<0) fgv=0;
                    fgv=fgv*10+Character.digit(c,10);
                  }
                  if (j<len && s.charAt(j)==',')
                  { k=j;
                    for (j=k+1; j<len && j<=k+2; j++)
                    { c=s.charAt(j);
                      if (!Character.isDigit(c)) break;
                      if (bgv<0) bgv=0;
                      bgv=bgv*10+Character.digit(c,10);
                    }
                  }
                  if (fgv>=0) fgColorIndexCurrent=fgv % 16;
                  if (bgv>=0) bgColorIndexCurrent=bgv % 16;
                  if (-1==fgv && -1==bgv)
                  { fgColorIndexCurrent=fgColorIndex;
                    bgColorIndexCurrent=bgColorIndex;
                  }
                  i=j-1;
                  start=i+1;
                }
                break;
              case MircMessage.REVERSE:
                int saveci=bgColorIndexCurrent;
                bgColorIndexCurrent=fgColorIndexCurrent;
                fgColorIndexCurrent=saveci;
                break;
              case MircMessage.RESET:
                boldCurrent=false;
                italicCurrent=false;
                underlineCurrent=false;
                bgColorIndexCurrent=bgColorIndex;
                fgColorIndexCurrent=fgColorIndex;
                break;
            }
            break;
        }
      } // end of for (int i=0; i<len; i++)
      
      if (start<len) // take the rest
        runs.addElement(new Run(s.substring(start,len),boldCurrent,italicCurrent,underlineCurrent,fgColorIndexCurrent,bgColorIndexCurrent));
        
      if (nick != null)
      {
      		Run run = (Run)runs.elementAt(0);
      		run.setNick(nick);
      		runs.setElementAt(run, 0);
      }
      
    }

    public int findEndOfHref(String s, int start)
    {
		int i = start;
		int len = s.length();

		if (s.charAt(i) == '#' || s.charAt(i) == '&')
		{
			/*
			** If hyperlink is a channel name,
			** parse until a reserved character is met.
			*/
			i++;
			boolean ok = true;
			
			while(i < len && ok)
			{
				ok = (s.charAt(i) > ' ') && (s.charAt(i) != ',');
				i++;
			}
				
			if (ok)
				return(i);
			else
				return(i - 1);
		}
		else
		{
			/*
			** If hyperlink is a FTP, HTTP, or WWW link,
			** first, check the end of hostname,
			** second, check the end of path.
			*/
			if (!s.substring(i, i + 4).toUpperCase().equals("WWW."))
				i = s.indexOf("//", i) + 2;

			String dns_chr = "@:ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-.";
			while(i < len && dns_chr.indexOf(Character.toUpperCase(s.charAt(i))) >= 0)
				i++;

			if (i < len && s.charAt(i) != '/')
				return(i);
			
			boolean path = true;
			while(i < len && path)
				path = s.charAt(i++) > ' ';
			
			if (path)
				return(i);
			else
				return(i - 1);
		}
    }

    public boolean isHref(String s, int i)
    {
		if (i < 0 || i >= s.length() || s.length() - i < 2)
			return(false);
			
    	char c = s.charAt(i);

		if ((c == '#' || c == '&') && !Character.isSpaceChar(s.charAt(i + 1)))
			return(true);
			
		if (s.length() < i + 8)
			return(false); // 8 chars at least (www.x.yy http://x)
			
		if (s.substring(i, i + 4).toUpperCase().equals("WWW."))
			return(true);
			
		if (s.substring(i, i + 7).toUpperCase().equals("HTTP://"))
			return(true);
			
		if (s.substring(i, i + 6).toUpperCase().equals("FTP://"))
			return(true);
			
//		if (s.substring(i).indexOf("@") >= 0)
//			return(true);

		return(false);
    }

    public void reconstruct() // reconstruct ContentLine (prepare for display)
    {
		int x = borderx;
		int y = -1; // inside a line think from top left as (0, 0)
		int line = 0;
		boolean display = true;
		
		int tabIndex = 0;
		
		for (Enumeration e = runs.elements(); e.hasMoreElements(); )
      	{
			Run run = (Run)e.nextElement();
			line = run.setXY(x, y, line);
			
			if (line > 0 && !canBreak)
			{
				line = 0;
				display = false;
			}
			run.setDisplay(display);
			
			if (tabs != null && tabIndex < tabs.size())
			{
				x = ((Integer)tabs.elementAt(tabIndex)).intValue();
				tabIndex++;
			}
			else
			{
				x = run.pixX1; //+fmSpaceFonts;
			}
			y = run.pixY1;
		}
		lineDY = line + 1;
    }

    int paint(Graphics g, int lineRevY)
    {
		if (lineRevY >= -lineDY + 1) // clipping of other edge in SmileyTextAreaArea.paint()
		{
			g.setColor(backgroundColor);
			if (mouse_over)
				g.setColor(mouseoverColor);
				
			if (line_selected)
				g.setColor(selectedColor);
				
			//int y1 = dimy - bordery - 1 - fmDescent - lineRevY * fmDY - (lineDY - 1) * fmDY;
			int y = dimy - bordery - (lineRevY + lineDY) * fmDY; // top
			y += offset;
			Rectangle r = g.getClipBounds();
			if (y > (r.y - fmDY) && y < (r.y + r.height + fmDY))
			{

				g.fillRect(2, y, dimx - 3, lineDY * fmDY);
				y += fmDY - fmDescent; // baseline of top text line
				int n = 0;
				for (Enumeration e = runs.elements(); e.hasMoreElements(); )
				{
					Run run = (Run)e.nextElement();
					if (run.display())
						run.paint(g, y, (n++) == 0);
				}
			}
		}
		return(lineRevY + lineDY);
    }

	public void setSelected(boolean b)
	{
		line_selected = b;
	}
	
	public boolean isSelected()
	{
		return(line_selected);
	}
	
	public void setMouseOver(boolean b)
	{
		mouse_over = b;
	}
	
    public void add(Run run)
    {
    	runs.addElement(run);
    }
    
  } // ============ End of class ContentLine ============

}
